#include <grub/err.h>
#include <grub/i18n.h>
#include <grub/efi/api.h>
#include <grub/efi/efi.h>
#include <grub/efi/tpcm.h>
#include <grub/mm.h>
#include <grub/verify.h>
#include <grub/term.h>
#include <grub/misc.h>
#include <grub/time.h>

GRUB_MOD_LICENSE ("GPLv3+");

#define TRANS(value)  (((value << 24 ) & 0xFF000000) | \
                       ((value <<  8 ) & 0x00FF0000) | \
                       ((value >>  8 ) & 0x0000FF00) | \
                       ((value >> 24 ) & 0x000000FF))

static grub_guid_t gIpmiInterfaceProtocolGuid = EFI_TPCM_GUID;
static grub_guid_t hash2_service_binding_guid = GRUB_EFI_HASH2_SERVICE_BINDING_PROTOCOL_GUID;
static grub_guid_t hash2_guid = GRUB_EFI_HASH2_PROTOCOL_GUID;
static grub_guid_t sm3_guid = GRUB_HASH_ALGORITHM_SM3_GUID;

static grub_efi_ipmi_interface_protocol_t *tpcm_ipmi;
static grub_efi_uint16_t grub_tcpm_file_type = GRUB_FILE_TYPE_NONE;

static grub_uint32_t bm_stage_base = 2000;
static grub_efi_uint8_t permissive = 0;

static grub_efi_handle_t
grub_efi_service_binding (grub_guid_t *service_binding_guid)
{
  grub_efi_service_binding_t *service;
  grub_efi_status_t status;
  grub_efi_handle_t child_dev = NULL;
  grub_efi_handle_t *handles;
  grub_efi_uintn_t num_handles;

  handles = grub_efi_locate_handle (GRUB_EFI_BY_PROTOCOL, service_binding_guid, 0, &num_handles);
  if (!handles)
    {
      grub_printf ("couldn't locate service binding protocol handles\n");
      return NULL;
    }

  service = grub_efi_open_protocol (handles[0], service_binding_guid, GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
  if (!service)
    {
      grub_printf ("couldn't open efi service binding protocol\n");
      return NULL;
    }

  status = service->create_child (service, &child_dev);
  if (status != GRUB_EFI_SUCCESS)
    {
      grub_printf ("Failed to create child device of efi service %x\n", status);
      return NULL;
    }

  return child_dev;
}

static inline void
util_dump_hex (const char *name, void *p, int bytes)
{

  int i = 0;
  char *data = p;
  int add_newline = 1;
  grub_dprintf ("tpcm", "%s length=%d:\n", name, bytes);
  if (bytes != 0)
    {
      grub_dprintf ("tpcm", "%02x ", (unsigned char)data[i]);
      i++;
    }
  while (i < bytes)
    {
      grub_dprintf ("tpcm", "%02x ", (unsigned char)data[i]);
      i++;
      if (i % 16 == 0)
        {
          grub_dprintf("tpcm", "\n");
          add_newline = 0;
        }
      else
          add_newline = 1;
    }
  if (add_newline)
    grub_dprintf("tpcm", "\n");
}

static grub_efi_status_t
grub_efi_hash (unsigned char *buf, grub_size_t size, unsigned char *content)
{
  grub_efi_status_t status = GRUB_EFI_SUCCESS;
  grub_efi_hash2_protocol_t *hash2;
  grub_efi_handle_t hash_handle;
  unsigned char output[DEFAULT_HASH_SIZE] = {0};
  grub_dprintf("tpcm", "grub_efi_hash binding service.\n");
  hash_handle = grub_efi_service_binding (&hash2_service_binding_guid);
  if (!hash_handle)
    {
      grub_dprintf ("tpcm", "hash2 service binding failed.\n");
      status = GRUB_EFI_NOT_FOUND;
      goto fail;
    }
  grub_dprintf("tpcm", "grub_efi_hash binding service success.\n");
  hash2 = grub_efi_open_protocol (hash_handle, &hash2_guid, GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
  if (!hash2)
    {
      grub_dprintf ("tpcm", "hash2 protocol open failed.\n");
      status =  GRUB_EFI_PROTOCOL_ERROR;
      goto fail;
    }
  grub_dprintf("tpcm", "grub_efi_hash get protocol success.\n");
  status = hash2->hash_init(hash2, &sm3_guid);
  if (status != GRUB_EFI_SUCCESS)
    {
      grub_dprintf("tpcm", "hash_init failed.\n");
      goto fail;
    }
  status = hash2->hash_update(hash2, buf, size);
  if (status != GRUB_EFI_SUCCESS)
    {
      grub_dprintf("tpcm", "hash_update failed.\n");
      goto fail;
    }
  status = hash2->hash_final(hash2, output);
  if (status != GRUB_EFI_SUCCESS)
    {
      grub_dprintf("tpcm", "hash_final failed.\n");
      goto fail;
    }
  util_dump_hex ("tpcm BIOS hash output: ", output, DEFAULT_HASH_SIZE);
  grub_memcpy(content, output, DEFAULT_HASH_SIZE);

fail:
  return status;
}

static grub_err_t
tpcm_ipmi_init (grub_file_t io,
		      enum grub_file_type type __attribute__ ((unused)),
		      void **context, enum grub_verify_flags *flags)
{
  *context = io->name;
  grub_tcpm_file_type = type & GRUB_FILE_TYPE_MASK;
  *flags |= GRUB_VERIFY_FLAGS_SINGLE_CHUNK;
  return GRUB_ERR_NONE;
}

static grub_efi_uint8_t
get_firmware_hash_content(unsigned char *buf, grub_size_t size, unsigned char *content)
{
  grub_efi_status_t status;

  grub_dprintf ("tpcm", "grub_efi_hash:\n");
  status = grub_efi_hash (buf, size, content);

  return status;
}

static grub_err_t
grub_tpcm_set_firmware_detailtype (grub_efi_uint8_t *type)
{
  switch (grub_tcpm_file_type)
    {
    case GRUB_FILE_TYPE_LINUX_KERNEL:
      *type = IPMI_FW_DETAIL_KERNEL;
      break;
    case GRUB_FILE_TYPE_LINUX_INITRD:
      *type = IPMI_FW_DETAIL_INITRD;
      break;
    case GRUB_FILE_TYPE_CONFIG:
      *type = IPMI_FW_DETAIL_GRUB_CFG;
      break;
    default:
      grub_dprintf ("tpcm", "%d is not a file type that TPCM cares about.\n", grub_tcpm_file_type);
      grub_tcpm_file_type = GRUB_FILE_TYPE_NONE;
      break;
    }

  return GRUB_ERR_NONE;
}

static void
grub_tpcm_fillup_content (OEM_BMC_MEASURE_REQUSET *request_data, unsigned char *output)
{
  grub_efi_uint32_t filename_len = 0;
  switch (grub_tcpm_file_type)
    {
    case GRUB_FILE_TYPE_LINUX_KERNEL:
      filename_len = grub_strlen("kernel");
      grub_memcpy ((grub_efi_uint8_t *)(request_data->FirmwareHashContent.uaObj),
		      "kernel", filename_len);
      break;
    case GRUB_FILE_TYPE_LINUX_INITRD:
      filename_len = grub_strlen("initrd");
      grub_memcpy ((grub_efi_uint8_t *)(request_data->FirmwareHashContent.uaObj),
		      "initrd", filename_len);
      break;
    case GRUB_FILE_TYPE_CONFIG:
      filename_len = grub_strlen("grub.cfg");
      grub_memcpy ((grub_efi_uint8_t *)(request_data->FirmwareHashContent.uaObj),
		      "grub.cfg", filename_len);
      break;
    default:
      grub_dprintf ("tpcm", "%d is not a file type that TPCM cares about.\n", grub_tcpm_file_type);
      break;
    }

  request_data->FirmwareHashContent.uiCmdTag = TRANS (TPCM_TAG_REQ_COMMAND);
  request_data->FirmwareHashContent.uiCmdLength = TRANS (sizeof (extern_simple_bmeasure_req_st));
  request_data->FirmwareHashContent.uiCmdCode = TRANS (TPCM_ORD_ExternSimpleBootMeasure);
  request_data->FirmwareHashContent.uiPcr = TRANS (0);

  grub_uint32_t stage_base = bm_stage_base++;
  request_data->FirmwareHashContent.uiStage = TRANS (stage_base);

  grub_memcpy ((grub_efi_uint8_t *)(request_data->FirmwareHashContent.uaDigest),
               output, DEFAULT_HASH_SIZE);
  request_data->FirmwareHashContent.uiObjLen = TRANS (filename_len);

  return;
}

static grub_efi_status_t
grub_tpcm_request_result (void)
{
  grub_efi_status_t status = GRUB_EFI_SUCCESS;
  grub_ipmi_cmd_header request = {IPMI_BMC_LUN,
                                  IPMI_NETFN_OEM,
                                  IPMI_CMD_GET_MEASURE_PARM};
  grub_efi_uint8_t response_length;
  OEM_BMC_GET_RESULT_REQUSET get_result_request_data;
  OEM_BMC_GET_RESULT_RESPONSE get_result_response_data;

  grub_efi_int16_t timeout_ms = GRUB_IPMI_TIMEOUT_MS;

  grub_memset (&get_result_request_data, 0, sizeof(get_result_request_data));
  grub_memset (&get_result_response_data, 0, sizeof(get_result_response_data));

  get_result_request_data.OemSignature[0] = 0xDB;
  get_result_request_data.OemSignature[1] = 0x07;
  get_result_request_data.OemSignature[2] = 0x00;
  get_result_request_data.SubCmd = IPMI_SUB_CMD_CONTROL_REQ;
  get_result_request_data.FirmwareType = IPMI_FW_OS;

  // TODO: we should not load files expect: grub.cfg vmlinuz and initrd
  grub_tpcm_set_firmware_detailtype (&(get_result_request_data.FirmwareDetailType));

  while (timeout_ms > 0)
    {
      response_length = sizeof (OEM_BMC_GET_RESULT_RESPONSE);
      grub_millisleep (200);
      timeout_ms -= 200;

      grub_dprintf ("tpcm", "get result request: request_size[%lu], response_length[%d]\n",
                    sizeof(get_result_request_data), response_length);

      status = tpcm_ipmi->excute_ipmi_cmd (tpcm_ipmi, request, &get_result_request_data,
                                           sizeof(get_result_request_data), &get_result_response_data,
                                           &response_length, NULL);
      if (status != GRUB_EFI_SUCCESS)
        {
          grub_dprintf ("tpcm", "excute_ipmi_cmd failed, request sub_cmd:%d, ret:%lu\n",
                        get_result_request_data.SubCmd, status);
          continue;
        }

      if (response_length == sizeof (OEM_BMC_GET_RESULT_RESPONSE) && \
          get_result_response_data.ControlResult != IPMI_MEASURE_UNKNOW)
        {
          grub_dprintf ("tpcm", "request ControlResult success, ControlResult:%d\n",
                        get_result_response_data.ControlResult);
          break;
        }
    }

  if (get_result_response_data.ControlResult == IPMI_MEASURE_SUCCESS)
    {
      return GRUB_EFI_SUCCESS;
    }
  else if (timeout_ms <= 0)
    {
      grub_dprintf ("tpcm", "request TPCM constrol result timout");
      return GRUB_EFI_TIMEOUT;
    }

  return GRUB_EFI_UNSUPPORTED;
}

static grub_err_t
grub_tpcm_log_event (unsigned char *buf, grub_size_t size, const char *description)
{
  grub_err_t err = GRUB_ERR_NONE;
  grub_efi_status_t status = GRUB_EFI_SUCCESS;
  grub_ipmi_cmd_header request = {IPMI_BMC_LUN,
                                  IPMI_NETFN_OEM,
                                  IPMI_CMD_GET_MEASURE_PARM};
  OEM_BMC_MEASURE_REQUSET *request_data = NULL;
  OEM_BMC_MEASURE_RESPONSE response_data;

  unsigned char output[DEFAULT_HASH_SIZE] = {0};
  grub_efi_uint8_t response_length = sizeof (OEM_BMC_MEASURE_RESPONSE);

  grub_memset (&response_data, 0, sizeof (response_data));

  request_data = grub_calloc (1, sizeof (OEM_BMC_MEASURE_REQUSET));
  if (!request_data)
    {
      grub_dprintf ("tpcm", "malloc request_data failed.\n");
      grub_error (GRUB_ERR_OUT_OF_MEMORY, "out of memory");
      return grub_errno;
    }

  request_data->OemSignature[0] = 0xDB;
  request_data->OemSignature[1] = 0x07;
  request_data->OemSignature[2] = 0x00;
  request_data->SubCmd = IPMI_SUB_CMD_MEASURE_REQ;
  request_data->FirmwareType = IPMI_FW_OS;
  request_data->FirmwareHashAlgoType = IPMI_FW_HASH_SM3; 

  grub_tpcm_set_firmware_detailtype (&(request_data->FirmwareDetailType));

  status = get_firmware_hash_content (buf, size, output);
  if (status != GRUB_EFI_SUCCESS)
    {
      if (permissive)
        grub_dprintf ("tpcm", "tpcm control switch turned off, ignore get firmware hash content failure.\n");
      else
        {
          grub_printf ("get firmware hash content failed\n");
          err = GRUB_ERR_BUG;
        }
      goto fail;
    }

  request_data->FirmwareHashLen = sizeof(extern_simple_bmeasure_req_st);
  grub_tpcm_fillup_content (request_data, output);

  status = tpcm_ipmi->excute_ipmi_cmd (tpcm_ipmi, request, request_data,
                                       sizeof (OEM_BMC_MEASURE_REQUSET), &response_data,
                                       &response_length, NULL);
  if (status != GRUB_EFI_SUCCESS)
    {
      if (permissive)
        grub_dprintf ("tpcm", "tpcm control switch turned off, ignore excute_ipmi_cmd failure.\n");
      else
        {
          err = grub_error (GRUB_ERR_BUG,
                            "excute_ipmi_cmd failed, request sub_cmd:0x%x, ret:%lu\n",
                            request_data->SubCmd, status);
        }
      goto fail;
    }
  grub_dprintf ("tpcm", "send tpcm measure request success\n");

  status = grub_tpcm_request_result ();
  if (status != GRUB_EFI_SUCCESS)
    {
      if (permissive)
        grub_dprintf ("tpcm", "tpcm control switch turned off, ignore measurement failure.\n");
      else
        {
          err = grub_error (GRUB_ERR_BAD_SIGNATURE, "bad tpcm signature");
          goto fail;
        }
    }
  else
    grub_dprintf ("tpcm", "tpcm hash verify success, file:%s\n", description);

 fail:
  if (request_data)
    {
      grub_free (request_data);
    }
  return err;
}

static grub_efi_uint8_t
tpcm_ipmi_get_switch (void)
{
  grub_efi_status_t status = GRUB_EFI_SUCCESS;
  grub_ipmi_cmd_header request = {IPMI_BMC_LUN,
                                  IPMI_NETFN_OEM,
                                  IPMI_CMD_GET_MEASURE_PARM};
  OEM_BMC_GET_SWITCH_REQUSET request_data;
  OEM_BMC_GET_SWITCH_RESPONSE response_data;
  grub_efi_uint8_t response_length;

  grub_memset (&request_data, 0, sizeof (request_data));
  grub_memset (&response_data, 0, sizeof (response_data));

  request_data.OemSignature[0] = 0xDB;
  request_data.OemSignature[1] = 0x07;
  request_data.OemSignature[2] = 0x00;
  request_data.SubCmd = IPMI_SUB_CMD_SWITCH_REQ;
  request_data.FirmwareType = IPMI_FW_OS;
  request_data.FirmwareDetailType = IPMI_FW_DETAIL_GRUB_CFG;

  response_length = sizeof (OEM_BMC_GET_SWITCH_RESPONSE);

  // TODO: 确认bios接口excute_ipmi_cmd, 请求开关控制结果这块代码是否需要轮询
  status = tpcm_ipmi->excute_ipmi_cmd (tpcm_ipmi, request, &request_data,
                                       sizeof(request_data), &response_data,
                                       &response_length, NULL);
  if (status != GRUB_EFI_SUCCESS)
    {
      grub_dprintf ("tpcm", "excute_ipmi_cmd failed, request sub_cmd:%d, ret:%lu\n",
                    request_data.SubCmd, status);
      /*  if we excute_ipmi_cmd, it could be the fllowing results:
       *  1. uefi have this interface, but did not implement it.
       *  2. uefi have implemented, but bmc did not support TPCM
       *  All of these situation should booting normally.
       */
      goto out;
    }

  if (response_data.ControlResult == IPMI_TPCM_OPEN || response_data.ControlResult == IPMI_TPCM_PERMISSIVE)
    {
      permissive = (response_data.ControlResult == IPMI_TPCM_PERMISSIVE) ? 1 : 0;
      grub_dprintf ("tpcm", "tpcm: Enabled, ControlResult: %d\n", response_data.ControlResult);
      return 1;
    }

 out:
  grub_dprintf ("tpcm", "tpcm: Disabled or Unknown, ControlResult: %d\n", response_data.ControlResult);
  return 0;
}

static grub_err_t
tpcm_ipmi_measure (unsigned char *buf, grub_size_t size, const char *description)
{
  if (tpcm_ipmi_get_switch())
    {
      grub_dprintf("tpcm", "hash file: %s\n", description);
      return grub_tpcm_log_event(buf, size, description);
    }

  return GRUB_ERR_NONE;
}

static grub_err_t
tpcm_ipmi_write (void *context __attribute__ ((unused)), void *buf, grub_size_t size)
{
  grub_err_t err;
  err = tpcm_ipmi_measure (buf, size, context);
  grub_tcpm_file_type = GRUB_FILE_TYPE_NONE;
  return err;
}

struct grub_file_verifier tpcm =
  {
    .name = "tpcm",
    .init = tpcm_ipmi_init,
    .write = tpcm_ipmi_write
  };


GRUB_MOD_INIT(tpcm)
{
  tpcm_ipmi = grub_efi_locate_protocol (&gIpmiInterfaceProtocolGuid, 0);
  if (!tpcm_ipmi)
    {
      grub_dprintf ("tpcm", "locate IpmiInterfaceProtocol failed, TPCM unsupported in the machine.\n");
      return;
    }
  grub_verifier_register (&tpcm);
}

GRUB_MOD_FINI(tpcm)
{
  grub_verifier_unregister (&tpcm);
}

